Preserve target order in generated project#1619
Conversation
|
Hi @yonaskolb I added this change to preserve target sorting as declared in specification, previously targets were always alphabetically sorted: let allTargets: [PBXTarget] = targetObjects.valueArray + targetAggregateObjects.valueArray
pbxProject.targets = allTargets
.sorted { $0.name < $1.name }I initially added new option flag to specify the order, but after some sleep and using this in my project, I had a (hopefully) better idea in mind 💡 I think this is much more intuitive for the user. |
|
@yonaskolb any questions, any suggestions about this? |
|
This would be a very welcome change! |
|
Thanks for working on this—preserving declaration order is a useful improvement. I found two resolution paths where the declared order is currently lost. First, targets:
Zebra:
type: framework
templates: [CrossPlatform]
Apple:
type: framework
platform: iOS
targetTemplates:
CrossPlatform:
platform: [iOS, tvOS]This generates the target order Second, Could the declaration keys be carried through the same variable, template, rename, and multiplatform transformations as the target dictionary? Regression tests covering a template-supplied platform array and an environment-expanded target key would catch both cases. |
The declaration order was computed from raw spec keys before
resolveProject, so it missed platform/name values inherited from
target templates, and keys containing ${VARIABLES} never matched
the expanded target dictionary — both cases silently fell back to
alphabetical order.
Instead of pre-computing final names, stamp each raw target with a
__declarationIndex before resolution. The index rides through the
variable, template, rename and multiplatform transformations along
with the rest of the target dictionary, and is read back off the
resolved targets (keyed by resolved name) to sort them. Declaration
keys are expanded with the same variables as the dictionary keys so
${VAR} target names stay in the order map.
Order extraction no longer throws: a spec whose contents can't be
re-parsed for ordering just falls back to alphabetical instead of
failing generation.
Adds regression tests for a template-supplied platform array, a
template-supplied name, and an environment-expanded target key.
|
@yonaskolb thanks for checking out my PR. I updated it to take the approach you suggested, the declaration keys are now carried through the same transformations as the target dictionary:
Added regression tests for both of your examples, plus a template-supplied |
Summary
Targets in the generated
.xcodeprojnow appear in the order they are declared in the source spec, for both YAML and JSON specs. Previously they were always sorted alphabetically, regardless of source order. This affects the Xcode sidebar andxcodebuild -listoutput.Motivation
XcodeGen always sorted targets alphabetically — both at parse time (
Project.swift:184) and when writing the final xcodeproj (PBXProjGenerator.swift:326). The only workaround was renaming/prefixing targets (01_App,02_Tests), which leaks into scheme names, product names, etc.The reason alphabetical sort existed in the first place: XcodeGen uses
Yams.loadfor YAML andJSONSerializationfor JSON, both of which return a SwiftDictionarythat discards source order. A deterministic sort was imposed to avoid nondeterministic output. Yams also offersYams.compose, which returns aNodetree that preserves mapping key order — and because JSON is a valid subset of YAML 1.2,Yams.composehandles JSON specs too. This PR uses it for order extraction on both formats, while leaving the mainJSONSerializationparse path intact.Since Xcode by itself allows you to order targets the way you want I think we should also have an option in Xcodegen to do the same.
Design
Yaml.swift: newloadOrderedTargetNames(path:)helper usesYams.composeto read the ordered keys under the top-leveltargets:mapping. Works for both.ymland.jsonspecs. Returns[]whentargets:is absent (e.g. pure include fragments that only contributesettings:/targetTemplates:).SpecFile: carries a per-filetargetDeclarationOrder: [String]. A newresolvedTargetDeclarationOrder()walks the include graph and concatenates orders, with first-occurrence-wins dedup — an include that declares a target keeps its slot even when the main spec overrides the target's content.Project.init: accepts an optionaltargetDeclarationOrder: [String] = []. BeforeresolveProjectruns, declaration keys are expanded throughexpandedTargetOrderto handle two transformations that happen inresolveProject:name:rename —IncludedTargetwithname: IncludedTargetNewcontributesIncludedTargetNew.Foowithplatform: [iOS, macOS]contributesFoo_iOSandFoo_macOS, respectingplatformPrefix/platformSuffix.Target: extractedmultiplatformTargetName(fromExpanded:key:platform:)so the multiplatform naming formula has a single source of truth — shared betweenresolveMultiplatformTargetsandexpandedTargetOrder.(orderIndex[name] ?? .max, name)using a precomputed[name: position]dictionary — declaration order first, alphabetical tie-break for anything unlisted.PBXProjGenerator: no longer sorts. Pullsproject.targetsin order, then appendsproject.aggregateTargets(which remain alphabetical since they live under a separate YAML/JSON key). Aggregate targets don't participate in declaration ordering; they always come last.Behaviour change
This is a behavior change for every project loaded from a spec file. Regenerating will reshuffle the Xcode sidebar and
xcodebuild -listoutput to match source order.Programmatically-built
Project(memberwise initProject(name:, targets: [...])) already preserved the array order as given — no sort. That path is unchanged.Example
→ Sidebar order:
MyApp,MyAppTests,MyAppUITests. Equivalent JSON spec behaves identically.Testing
All 76 tests pass. Changes:
Tests/Fixtures/target_ordering_test.ymlandTests/Fixtures/target_ordering_test.json, andSpecLoadingTests.testTargetDeclarationOrderwith two cases — YAML and JSON — each loading a spec with non-alphabetical keys and asserting source order is preserved.PBXProjGeneratorTests.testTargetOrdering— 3 cases: declaration order honored from a YAML-parsedProject; order preserved verbatim in a programmatically-builtProject; aggregate targets land after native targets.SpecLoadingTests.testSpecLoader(thepaths_test.ymlcase, deep include chain) that hard-coded the old alphabetical order.Tests/Fixtures/CarthageProject,Tests/Fixtures/SPM, andTests/Fixtures/TestProjectpbxproj files to reflect the new order — these are checked-in generator outputs.Docs
CHANGELOG.md— entry under "Next Version" → "Changed", noting the behavior change.Test plan
swift buildcleanswift test— 76/76 passxcodebuild -listoutput match source